﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Globalization;
using System.Collections.ObjectModel;
using System.Diagnostics;

namespace Watson.TemporaryFilesDeleter
{
    /// <summary>
    /// Provides the functionality to delete temporary files from a specified
    /// location.
    /// </summary>
    public class FileDeleter
    {
        #region Private Fields

        /// <summary>
        /// A collection of errors that occurred during the deletion operation.
        /// </summary>
        private IList<Exception> errors = new List<Exception>();
        
        /// <summary>
        /// A counter of items processed.
        /// </summary>
        private long progress;

        /// <summary>
        /// The percentage, from 0 to 100, of the deletion operation that is 
        /// complete.
        /// </summary>
        private byte progressPercentage;
        
        /// <summary>
        /// A string describing the current activity of the file deleter.
        /// </summary>
        private string currentActivity;
        
        /// <summary>
        /// The filter collection used when deleting files.
        /// </summary>
        private ICollection<IFileFilter> filters = new List<IFileFilter>();

        /// <summary>
        /// A filtered collection of files to delete.
        /// </summary>
        private ICollection<FileInfo> filteredFiles;

        /// <summary>
        /// A filtered collection of folders to delete.
        /// </summary>
        private ICollection<DirectoryInfo> filteredFolders;

        #endregion

        #region Public Events

        /// <summary>
        /// Occurs when the current activity changes.
        /// </summary>
        public event EventHandler<CurrentActivityChangedEventArgs> 
            CurrentActivityChanged;

        /// <summary>
        /// Occurs when an error was encountered (and handled) during the
        /// deletion operation.
        /// </summary>
        public event EventHandler<ErrorEncounteredEventArgs> ErrorEncountered;

        /// <summary>
        /// Occurs when the percentage progress changes.
        /// </summary>
        public event EventHandler<ProgressChangedEventArgs> ProgressChanged;

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets a string describing the current activity.
        /// </summary>
        public string CurrentActivity
        {
            get
            {
                return this.currentActivity;
            }
            protected set
            {
                if (this.currentActivity != value)
                {
                    this.currentActivity = value;

                    OnCurrentActivityChanged();
                }
            }
        }

        /// <summary>
        /// Gets a read-only collection of errors that occurred during the
        /// deletion operation.
        /// </summary>
        public ICollection<Exception> Errors
        {
            get { return new ReadOnlyCollection<Exception>(this.errors); }
        }
        
        /// <summary>
        /// Gets the filter collection used when deleting files.
        /// </summary>
        public ICollection<IFileFilter> Filters
        {
            get { return this.filters; }
        }        

        /// <summary>
        /// Gets or sets the path to the temporary files location (folder)
        /// containing the files to be deleted.
        /// </summary>
        public string Folder { get; set; }

        /// <summary>
        /// Gets the percentage, from 0 to 100, of the deletion operation that
        /// is complete.
        /// </summary>
        public byte ProgressPercentage
        {
            get
            {
                return this.progressPercentage;
            }
            protected set
            {
                if (this.progressPercentage != value)
                {
                    this.progressPercentage = value;

                    OnProgressPercentageChanged();
                }
            }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Adds an error that occurred during the deletion operation to the
        /// errors collection.
        /// </summary>
        /// <param name="error">
        /// The error to add to the errors collection.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// <paramref name="error"/> was null.
        /// </exception>
        private void AddError(Exception error)
        {
            if (error == null)
            {
                throw new ArgumentNullException("error");
            }


            this.errors.Add(error);

            OnErrorAdded(error);

            Log.Instance.Write(LogLevel.Warning, error);
        }
        
        /// <summary>
        /// Builds the filtered list of files and folders to delete.
        /// </summary>
        private void BuildFilteredCollections()
        {
            // Set current activity.
            this.CurrentActivity = "Examining temporary files location.";
            
            
            // Initialise collections.
            this.filteredFiles   = new List<FileInfo>();
            this.filteredFolders = new List<DirectoryInfo>();


            // Obtain all files from the temporary files location.
            string[] unfilteredFiles = 
                Directory.GetFiles(this.Folder,
                                   "*",
                                   SearchOption.AllDirectories);

            // Add all found files to the filtered files collection.
            foreach (var file in unfilteredFiles)
            {
                FileInfo fileInfo = new FileInfo(file);

                this.filteredFiles.Add(fileInfo);
            }

            // Apply each filter in turn to the filtered files collection.
            foreach (var filter in this.Filters)
            {
                this.filteredFiles = filter.ApplyFilter(this.filteredFiles);
            }


            // Obtain all folders from the temporary files location.
            string[] unfilteredFolders =
                Directory.GetDirectories(this.Folder,
                                         "*",
                                         SearchOption.AllDirectories);

            // Order by deepest nested first.
            var orderedUnfilteredFolders = 
                from folder in unfilteredFolders
                orderby Directory.GetDirectories(
                            folder, 
                            "*",
                            SearchOption.AllDirectories).Length
                select folder;

            // Add all found folders to the filtered folders collection.
            foreach (var folder in orderedUnfilteredFolders)
            {
                DirectoryInfo directoryInfo = new DirectoryInfo(folder); 
                
                this.filteredFolders.Add(directoryInfo);
            }

            // Apply each filter in turn... no, wait, we don't filter folders.
        }

        /// <summary>
        /// Deletes the files and folders in the filtered collections.
        /// </summary>
        private void DeleteFiles()
        {
            // Reset errors.
            this.errors.Clear();

            
            // Reset progress.
            this.ResetProgress();

            
            // Delete files.
            foreach (FileInfo file in this.filteredFiles)
            {
                this.CurrentActivity = "Deleting \""
                                     + file.FullName
                                     + "\".";

                try
                {
                    file.Delete();
                }
                catch (Exception ex)
                {
                    // TODO: And do what with the caught exception?
                    Debug.WriteLine(ex.GetType().FullName + ": " + ex.Message);

                    this.AddError(ex);
                }
                finally
                {
                    this.IncrementProgress();
                }
            }

            
            // Delete folders.
            foreach (DirectoryInfo folder in this.filteredFolders)
            {
                // Only delete a folder if it does not contain any files.
                if (!folder.GetFiles("*", SearchOption.AllDirectories).Any())
                {
                    this.CurrentActivity = "Deleting \"" 
                                         + folder.FullName
                                         + "\".";                    
                    
                    try
                    {
                        folder.Delete();
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex.GetType().FullName
                                        + ": "
                                        + ex.Message);
                        
                        this.AddError(ex);
                    }
                }

                this.IncrementProgress();
            }
        }

        /// <summary>
        /// Increments the number of items processed by one.
        /// </summary>
        private void IncrementProgress()
        {
            // Increment progress counter.
            this.progress++;


            // Calculate progress percentage.
            var totalItems = this.filteredFiles.Count
                           + this.filteredFolders.Count;

            var percentProgress = (Convert.ToSingle(this.progress) / Convert.ToSingle(totalItems)) * 100;

            this.ProgressPercentage = Convert.ToByte(percentProgress);
        }

        /// <summary>
        /// Resets the progress counter.
        /// </summary>
        private void ResetProgress()
        {
            // Reset counter.
            this.progress = 0;

            // Reset percentage progress.
            this.ProgressPercentage = 0;
        }

        /// <summary>
        /// Raises the <see cref="CurrentActivityChanged"/> event.
        /// </summary>
        private void OnCurrentActivityChanged()
        {
            EventHandler<CurrentActivityChangedEventArgs> temp = 
                CurrentActivityChanged;
            
            if (temp != null)
            {
                CurrentActivityChangedEventArgs eventArgs = 
                    new CurrentActivityChangedEventArgs(this.CurrentActivity);

                temp(this, eventArgs);
            }
        }

        /// <summary>
        /// Raises the <see cref="ErrorEncountered"/> event.
        /// </summary>
        /// <param name="error">
        /// The error that was added.
        /// </param>
        /// <exception cref="System.ArgumentNullException">
        /// <paramref name="error"/> was null.
        /// </exception>
        private void OnErrorAdded(Exception error)
        {
            if (error == null)
            {
                throw new ArgumentNullException("error");
            }


            EventHandler<ErrorEncounteredEventArgs> temp = ErrorEncountered;

            if (temp != null)
            {
                ErrorEncounteredEventArgs eventArgs = 
                    new ErrorEncounteredEventArgs(error);
                
                temp(this, eventArgs);
            }
        }
        
        /// <summary>
        /// Raises the <see cref="ProgressChanged"/> event.
        /// </summary>
        private void OnProgressPercentageChanged()
        {
            EventHandler<ProgressChangedEventArgs> temp = ProgressChanged;
            
            if (temp != null)
            {
                ProgressChangedEventArgs eventArgs =
                    new ProgressChangedEventArgs(this.ProgressPercentage);
                
                temp(this, eventArgs);
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Executes the file deletion operation. The files in
        /// <see cref="Folder"/> will be deleted. Any optional filters
        /// specified in <see cref="Filters"/> will be used when determining
        /// which files are to be deleted.
        /// </summary>
        /// <exception cref="System.InvalidOperation">
        /// <see cref="Folder"/> was not set.
        /// </exception>
        /// <exception cref="System.IO.DirectoryNotFoundException">
        /// <see cref="Folder"/> was not found.
        /// </exception>
        public void Execute()
        {
            if (String.IsNullOrWhiteSpace(this.Folder))
            {
                throw new InvalidOperationException("Folder is not set.");
            }
            else if(!Directory.Exists(this.Folder))
            {
                string message = 
                    String.Format(CultureInfo.CurrentCulture,
                                  "Directory not found. ({0}).",
                                  this.Folder);

                throw new DirectoryNotFoundException(message);
            }


            // Build our filtered collections of files and folders to work on.
            this.BuildFilteredCollections();

            // Delete the files and folders.
            this.DeleteFiles();
        }

        #endregion

        #region Public Constructors

        /// <summary>
        /// Initialises a new instance of the FileDeleter class.
        /// </summary>
        public FileDeleter()
        { }

        #endregion
    }
}
